Linux中國

學慣用 Git 變基來改變歷史!

Git 核心的附加價值之一就是編輯歷史記錄的能力。與將歷史記錄視為神聖的記錄的版本控制系統不同,在 Git 中,我們可以修改歷史記錄以適應我們的需要。這為我們提供了很多強大的工具,讓我們可以像使用重構來維護良好的軟體設計實踐一樣,編織良好的提交歷史。這些工具對於新手甚至是有經驗的 Git 用戶來說可能會有些令人生畏,但本指南將幫助我們揭開強大的 git-rebase 的神秘面紗。

值得注意的是:一般建議不要修改公共分支、共享分支或穩定分支的歷史記錄。編輯特性分支和個人分支的歷史記錄是可以的,編輯還沒有推送的提交也是可以的。在編輯完提交後,可以使用 git push -f 來強制推送你的修改到個人分支或特性分支。

儘管有這麼可怕的警告,但值得一提的是,本指南中提到的一切都是非破壞性操作。實際上,在 Git 中永久丟失數據是相當困難的。本指南結尾介紹了在犯錯誤時進行糾正的方法。

設置沙盒

我們不想破壞你的任何實際的版本庫,所以在整個指南中,我們將使用一個沙盒版本庫。運行這些命令來開始工作。 1

git init /tmp/rebase-sandbox
cd /tmp/rebase-sandbox
git commit --allow-empty -m"Initial commit"

如果你遇到麻煩,只需運行 rm -rf /tmp/rebase-sandbox,並重新運行這些步驟即可重新開始。本指南的每一步都可以在新的沙箱上運行,所以沒有必要重做每個任務。

修正最近的提交

讓我們從簡單的事情開始:修復你最近的提交。讓我們向沙盒中添加一個文件,並犯個錯誤。

echo "Hello wrold!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"

修復這個錯誤是非常容易的。我們只需要編輯文件,然後用 --amend 提交就可以了,就像這樣:

echo "Hello world!" >greeting.txt
git commit -a --amend

指定 -a 會自動將所有 Git 已經知道的文件進行暫存(例如 Git 添加的),而 --amend 會將更改的內容壓扁到最近的提交中。保存並退出你的編輯器(如果需要,你現在可以修改提交信息)。你可以通過運行 git show 看到修復的提交。

commit f5f19fbf6d35b2db37dcac3a55289ff9602e4d00 (HEAD -> master)
Author: Drew DeVault 
Date:   Sun Apr 28 11:09:47 2019 -0400

    Add greeting.txt

diff --git a/greeting.txt b/greeting.txt
new file mode 100644
index 0000000..cd08755
--- /dev/null
+++ b/greeting.txt
@@ -0,0 +1 @@
+Hello world!

修復較舊的提交

--amend 僅適用於最近的提交。如果你需要修正一個較舊的提交會怎麼樣?讓我們從相應地設置沙盒開始:

echo "Hello!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"

echo "Goodbye world!" >farewell.txt
git add farewell.txt
git commit -m"Add farewell.txt"

看起來 greeting.txt 像是丟失了 "world"。讓我們正常地寫個提交來解決這個問題:

echo "Hello world!" >greeting.txt
git commit -a -m"fixup greeting.txt"

現在文件看起來正確,但是我們的歷史記錄可以更好一點 —— 讓我們使用新的提交來「修復」(fixup)最後一個提交。為此,我們需要引入一個新工具:互動式變基。我們將以這種方式編輯最後三個提交,因此我們將運行 git rebase -i HEAD~3-i 代表互動式)。這樣會打開文本編輯器,如下所示:

pick 8d3fc77 Add greeting.txt
pick 2a73a77 Add farewell.txt
pick 0b9d0bb fixup greeting.txt

# Rebase f5f19fb..0b9d0bb onto f5f19fb (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# f, fixup <commit> = like "squash", but discard this commit&apos;s log message

這是變基計劃,通過編輯此文件,你可以指導 Git 如何編輯歷史記錄。我已經將該摘要削減為僅與變基計劃這一部分相關的細節,但是你可以在文本編輯器中瀏覽完整的摘要。

當我們保存並關閉編輯器時,Git 將從其歷史記錄中刪除所有這些提交,然後一次執行一行。默認情況下,它將選取(pick)每個提交,將其從堆中召喚出來並添加到分支中。如果我們對此文件根本沒有做任何編輯,則將直接回到起點,按原樣選取每個提交。現在,我們將使用我最喜歡的功能之一:修復(fixup)。編輯第三行,將操作從 pick 更改為 fixup,並將其立即移至我們要「修復」的提交之後:

pick 8d3fc77 Add greeting.txt
fixup 0b9d0bb fixup greeting.txt
pick 2a73a77 Add farewell.txt

技巧:我們也可以只用 f 來縮寫它,以加快下次的速度。

保存並退出編輯器,Git 將運行這些命令。我們可以檢查日誌以驗證結果:

$ git log -2 --oneline
fcff6ae (HEAD -> master) Add farewell.txt
a479e94 Add greeting.txt

將多個提交壓扁為一個

在工作時,當你達到較小的里程碑或修復以前的提交中的錯誤時,你可能會發現寫很多提交很有用。但是,在將你的工作合併到 master 分支之前,將這些提交「壓扁」(squash)到一起以使歷史記錄更清晰可能很有用。為此,我們將使用「壓扁」(squash)操作。讓我們從編寫一堆提交開始,如果要加快速度,只需複製並粘貼這些:

git checkout -b squash
for c in H e l l o , &apos; &apos; w o r l d; do
    echo "$c" >>squash.txt
    git add squash.txt
    git commit -m"Add &apos;$c&apos; to squash.txt"
done

要製作出一個寫著 「Hello,world」 的文件,要做很多事情!讓我們開始另一個互動式變基,將它們壓扁在一起。請注意,我們首先簽出了一個分支來進行嘗試。因此,因為我們使用 git rebase -i master 進行的分支,我們可以快速變基所有提交。結果:

pick 1e85199 Add &apos;H&apos; to squash.txt
pick fff6631 Add &apos;e&apos; to squash.txt
pick b354c74 Add &apos;l&apos; to squash.txt
pick 04aaf74 Add &apos;l&apos; to squash.txt
pick 9b0f720 Add &apos;o&apos; to squash.txt
pick 66b114d Add &apos;,&apos; to squash.txt
pick dc158cd Add &apos; &apos; to squash.txt
pick dfcf9d6 Add &apos;w&apos; to squash.txt
pick 7a85f34 Add &apos;o&apos; to squash.txt
pick c275c27 Add &apos;r&apos; to squash.txt
pick a513fd1 Add &apos;l&apos; to squash.txt
pick 6b608ae Add &apos;d&apos; to squash.txt

# Rebase 1af1b46..6b608ae onto 1af1b46 (12 commands)
#
# Commands:
# p, pick <commit> = use commit
# s, squash <commit> = use commit, but meld into previous commit

技巧:你的本地 master 分支獨立於遠程 master 分支而發展,並且 Git 將遠程分支存儲為 origin/master。結合這種技巧,git rebase -i origin/master 通常是一種非常方便的方法,可以變基所有尚未合併到上游的提交!

我們將把所有這些更改壓扁到第一個提交中。為此,將第一行除外的每個「選取」(pick)操作都更改為「壓扁」(squash),如下所示:

pick 1e85199 Add &apos;H&apos; to squash.txt
squash fff6631 Add &apos;e&apos; to squash.txt
squash b354c74 Add &apos;l&apos; to squash.txt
squash 04aaf74 Add &apos;l&apos; to squash.txt
squash 9b0f720 Add &apos;o&apos; to squash.txt
squash 66b114d Add &apos;,&apos; to squash.txt
squash dc158cd Add &apos; &apos; to squash.txt
squash dfcf9d6 Add &apos;w&apos; to squash.txt
squash 7a85f34 Add &apos;o&apos; to squash.txt
squash c275c27 Add &apos;r&apos; to squash.txt
squash a513fd1 Add &apos;l&apos; to squash.txt
squash 6b608ae Add &apos;d&apos; to squash.txt

保存並關閉編輯器時,Git 會考慮片刻,然後再次打開編輯器以修改最終的提交消息。你會看到以下內容:

# This is a combination of 12 commits.
# This is the 1st commit message:

Add &apos;H&apos; to squash.txt

# This is the commit message #2:

Add &apos;e&apos; to squash.txt

# This is the commit message #3:

Add &apos;l&apos; to squash.txt

# This is the commit message #4:

Add &apos;l&apos; to squash.txt

# This is the commit message #5:

Add &apos;o&apos; to squash.txt

# This is the commit message #6:

Add &apos;,&apos; to squash.txt

# This is the commit message #7:

Add &apos; &apos; to squash.txt

# This is the commit message #8:

Add &apos;w&apos; to squash.txt

# This is the commit message #9:

Add &apos;o&apos; to squash.txt

# This is the commit message #10:

Add &apos;r&apos; to squash.txt

# This is the commit message #11:

Add &apos;l&apos; to squash.txt

# This is the commit message #12:

Add &apos;d&apos; to squash.txt

# Please enter the commit message for your changes. Lines starting
# with &apos;#&apos; will be ignored, and an empty message aborts the commit.
#
# Date:      Sun Apr 28 14:21:56 2019 -0400
#
# interactive rebase in progress; onto 1af1b46
# Last commands done (12 commands done):
#    squash a513fd1 Add &apos;l&apos; to squash.txt
#    squash 6b608ae Add &apos;d&apos; to squash.txt
# No commands remaining.
# You are currently rebasing branch &apos;squash&apos; on &apos;1af1b46&apos;.
#
# Changes to be committed:
#   new file:   squash.txt
#

默認情況下,這是所有要壓扁的提交的消息的組合,但是像這樣將其保留肯定不是你想要的。不過,舊的提交消息在編寫新的提交消息時可能很有用,所以放在這裡以供參考。

提示:你在上一節中了解的「修復」(fixup)命令也可以用於此目的,但它會丟棄壓扁的提交的消息。

讓我們刪除所有內容,並用更好的提交消息替換它,如下所示:

Add squash.txt with contents "Hello, world"

# Please enter the commit message for your changes. Lines starting
# with &apos;#&apos; will be ignored, and an empty message aborts the commit.
#
# Date:      Sun Apr 28 14:21:56 2019 -0400
#
# interactive rebase in progress; onto 1af1b46
# Last commands done (12 commands done):
#    squash a513fd1 Add &apos;l&apos; to squash.txt
#    squash 6b608ae Add &apos;d&apos; to squash.txt
# No commands remaining.
# You are currently rebasing branch &apos;squash&apos; on &apos;1af1b46&apos;.
#
# Changes to be committed:
#   new file:   squash.txt
#

保存並退出編輯器,然後檢查你的 Git 日誌,成功!

commit c785f476c7dff76f21ce2cad7c51cf2af00a44b6 (HEAD -> squash)
Author: Drew DeVault
Date:   Sun Apr 28 14:21:56 2019 -0400

    Add squash.txt with contents "Hello, world"

在繼續之前,讓我們將所做的更改拉入 master 分支中,並擺脫掉這一草稿。我們可以像使用 git merge 一樣使用 git rebase,但是它避免了創建合併提交:

git checkout master
git rebase squash
git branch -D squash

除非我們實際上正在合併無關的歷史記錄,否則我們通常希望避免使用 git merge。如果你有兩個不同的分支,則 git merge 對於記錄它們合併的時間非常有用。在正常工作過程中,變基通常更為合適。

將一個提交拆分為多個

有時會發生相反的問題:一個提交太大了。讓我們來看一看拆分它們。這次,讓我們寫一些實際的代碼。從一個簡單的 C 程序 2 開始(你仍然可以將此代碼段複製並粘貼到你的 shell 中以快速執行此操作):

cat <<EOF >main.c
int main(int argc, char *argv[]) {
    return 0;
}
EOF

首先提交它:

git add main.c
git commit -m"Add C program skeleton"

然後把這個程序擴展一些:

cat <<EOF >main.c
#include &ltstdio.h>

const char *get_name() {
    static char buf[128];
    scanf("%s", buf);
    return buf;
}

int main(int argc, char *argv[]) {
    printf("What&apos;s your name? ");
    const char *name = get_name();
    printf("Hello, %s!n", name);
    return 0;
}
EOF

提交之後,我們就可以準備學習如何將其拆分:

git commit -a -m"Flesh out C program"

第一步是啟動互動式變基。讓我們用 git rebase -i HEAD~2 來變基這兩個提交,給出的變基計劃如下:

pick 237b246 Add C program skeleton
pick b3f188b Flesh out C program

# Rebase c785f47..b3f188b onto c785f47 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# e, edit <commit> = use commit, but stop for amending

將第二個提交的命令從 pick 更改為 edit,然後保存並關閉編輯器。Git 會考慮一秒鐘,然後向你建議:

Stopped at b3f188b...  Flesh out C program
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue

我們可以按照以下說明為提交添加新的更改,但我們可以通過運行 git reset HEAD^ 來進行「軟重置」 3 。如果在此之後運行 git status,你將看到它取消了提交最新的提交,並將其更改添加到工作樹中:

Last commands done (2 commands done):
   pick 237b246 Add C program skeleton
   edit b3f188b Flesh out C program
No commands remaining.
You are currently splitting a commit while rebasing branch &apos;master&apos; on &apos;c785f47&apos;.
  (Once your working directory is clean, run "git rebase --continue")

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

  modified:   main.c

no changes added to commit (use "git add" and/or "git commit -a")

為了對此進行拆分,我們將進行互動式提交。這使我們能夠選擇性地僅提交工作樹中的特定更改。運行 git commit -p 開始此過程,你將看到以下提示:

diff --git a/main.c b/main.c
index b1d9c2c..3463610 100644
--- a/main.c
+++ b/main.c
@@ -1,3 +1,14 @@
+#include &ltstdio.h>
+
+const char *get_name() {
+    static char buf[128];
+    scanf("%s", buf);
+    return buf;
+}
+
 int main(int argc, char *argv[]) {
+    printf("What&apos;s your name? ");
+    const char *name = get_name();
+    printf("Hello, %s!n", name);
     return 0;
 }
Stage this hunk [y,n,q,a,d,s,e,?]?

Git 僅向你提供了一個「大塊」(即單個更改)以進行提交。不過,這太大了,讓我們使用 s 命令將這個「大塊」拆分成較小的部分。

Split into 2 hunks.
@@ -1 +1,9 @@
+#include <stdio.h>
+
+const char *get_name() {
+    static char buf[128];
+    scanf("%s", buf);
+    return buf;
+}
+
 int main(int argc, char *argv[]) {
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

提示:如果你對其他選項感到好奇,請按 ? 匯總顯示。

這個大塊看起來更好:單一、獨立的更改。讓我們按 y 來回答問題(並暫存那個「大塊」),然後按 q 以「退出」互動式會話並繼續進行提交。會彈出編輯器,要求輸入合適的提交消息。

Add get_name function to C program

# Please enter the commit message for your changes. Lines starting
# with &apos;#&apos; will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto c785f47
# Last commands done (2 commands done):
#    pick 237b246 Add C program skeleton
#    edit b3f188b Flesh out C program
# No commands remaining.
# You are currently splitting a commit while rebasing branch &apos;master&apos; on &apos;c785f47&apos;.
#
# Changes to be committed:
#   modified:   main.c
#
# Changes not staged for commit:
#   modified:   main.c
#

保存並關閉編輯器,然後我們進行第二次提交。我們可以執行另一次互動式提交,但是由於我們只想在此提交中包括其餘更改,因此我們將執行以下操作:

git commit -a -m"Prompt user for their name"
git rebase --continue

最後一條命令告訴 Git 我們已經完成了此提交的編輯,並繼續執行下一個變基命令。這樣就行了!運行 git log 來查看你的勞動成果:

$ git log -3 --oneline
fe19cc3 (HEAD -> master) Prompt user for their name
659a489 Add get_name function to C program
237b246 Add C program skeleton

重新排序提交

這很簡單。讓我們從設置沙箱開始:

echo "Goodbye now!" >farewell.txt
git add farewell.txt
git commit -m"Add farewell.txt"

echo "Hello there!" >greeting.txt
git add greeting.txt
git commit -m"Add greeting.txt"

echo "How&apos;re you doing?" >inquiry.txt
git add inquiry.txt
git commit -m"Add inquiry.txt"

現在 git log 看起來應如下所示:

f03baa5 (HEAD -> master) Add inquiry.txt
a4cebf7 Add greeting.txt
90bb015 Add farewell.txt

顯然,這都是亂序。讓我們對過去的 3 個提交進行互動式變基來解決此問題。運行 git rebase -i HEAD~3,這個變基規劃將出現:

pick 90bb015 Add farewell.txt
pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt

# Rebase fe19cc3..f03baa5 onto fe19cc3 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
#
# These lines can be re-ordered; they are executed from top to bottom.

現在,解決方法很簡單:只需按照你希望提交出現的順序重新排列這些行。應該看起來像這樣:

pick a4cebf7 Add greeting.txt
pick f03baa5 Add inquiry.txt
pick 90bb015 Add farewell.txt

保存並關閉你的編輯器,而 Git 將為你完成其餘工作。請注意,在實踐中這樣做可能會導致衝突,參看下面章節以獲取解決衝突的幫助。

git pull –rebase

如果你一直在由上游更新的分支 <branch>(比如說原始遠程)上做一些提交,通常 git pull 會創建一個合併提交。在這方面,git pull 的默認行為等同於:

git fetch origin <branch>
git merge origin/<branch>

假設本地分支 <branch> 配置為從原始遠程跟蹤 <branch> 分支,即:

$ git config branch.<branch>.remote
origin
$ git config branch.<branch>.merge
refs/heads/<branch>

還有另一種選擇,它通常更有用,並且會讓歷史記錄更清晰:git pull --rebase。與合併方式不同,這基本上 4 等效於以下內容:

git fetch origin
git rebase origin/<branch>

合併方式更簡單易懂,但是如果你了解如何使用 git rebase,那麼變基方式幾乎可以做到你想要做的任何事情。如果願意,可以將其設置為默認行為,如下所示:

git config --global pull.rebase true

當你執行此操作時,從技術上講,你在應用我們在下一節中討論的過程……因此,讓我們也解釋一下故意執行此操作的含義。

使用 git rebase 來變基

具有諷刺意味的是,我最少使用的 Git 變基功能是它以之命名的功能:變基分支。假設你有以下分支:

A--B--C--D--> master
   --E--F--> feature-1
      --G--> feature-2

事實證明,feature-2 不依賴於 feature-1 的任何更改,它依賴於提交 E,因此你可以將其作為基礎脫離 master。因此,解決方法是:

git rebase --onto master feature-1 feature-2

非互動式變基對所有牽連的提交都執行默認操作(pick 5 ,它只是簡單地將不在 feature-1 中的 feature-2 中提交重放到 master 上。你的歷史記錄現在看起來像這樣:

A--B--C--D--> master
   |     --G--> feature-2
   --E--F--> feature-1

解決衝突

解決合併衝突的詳細信息不在本指南的範圍內,將來請你注意另一篇指南。假設你熟悉通常的解決衝突的方法,那麼這裡是專門適用於變基的部分。

有時,在進行變基時會遇到合併衝突,你可以像處理其他任何合併衝突一樣處理該衝突。Git 將在受影響的文件中設置衝突標記,git status 將顯示你需要解決的問題,並且你可以使用 git addgit rm 將文件標記為已解決。但是,在 git rebase 的上下文中,你應該注意兩個選項。

首先是如何完成衝突解決。解決由於 git merge 引起的衝突時,與其使用 git commit 那樣的命令,更適當的變基命令是 git rebase --continue。但是,還有一個可用的選項:git rebase --skip。 這將跳過你正在處理的提交,它不會包含在變基中。這在執行非交互性變基時最常見,這時 Git 不會意識到它從「其他」分支中提取的提交是與「我們」分支上衝突的提交的更新版本。

幫幫我! 我把它弄壞了!

毫無疑問,變基有時會很難。如果你犯了一個錯誤,並因此而丟失了所需的提交,那麼可以使用 git reflog 來節省下一天的時間。運行此命令將向你顯示更改一個引用(即分支和標記)的每個操作。每行顯示你的舊引用所指向的內容,你可對你認為丟失的 Git 提交執行 git cherry-pickgit checkoutgit show 或任何其他操作。

  1. 我們添加了一個空的初始提交以簡化本教程的其餘部分,因為要對版本庫的初始提交進行變基需要特殊的命令(即git rebase --root)。
  2. 如果要編譯此程序,請運行 cc -o main main.c,然後運行 ./main 查看結果。
  3. 實際上,這是「混合重置」。「軟重置」(使用 git reset --soft 完成)將暫存更改,因此你無需再次 git add 添加它們,並且可以一次性提交所有更改。這不是我們想要的。我們希望選擇性地暫存部分更改,以拆分提交。
  4. 實際上,這取決於上游分支本身是否已變基或刪除/壓扁了某些提交。git pull --rebase 嘗試通過在 git rebasegit merge-base 中使用 「 復刻點 fork-point 」 機制來從這種情況中恢復,以避免變基非本地提交。
  5. 實際上,這取決於 Git 的版本。直到 2.26.0 版,默認的非交互行為以前與交互行為稍有不同,這種方式通常並不重要。

via: https://git-rebase.io/

作者:git-rebase 選題:lujun9972 譯者:wxy 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國